[初心者向け]AWS LambdaでOpenAI API のFunction Callingを試してみた
はじめに
OpenAI API のFunction Callingを触ったことがなかったため、AWS Lambdaを使いを試してみました。
Function Callingとは、ユーザーから受け取った入力から、事前に定義した呼び出すべき関数を判断して、関数の入力形式通りにJSON形式で出力する機能です。
メリットとしては、指定した型に沿ってJSON形式で出力してくれるため、外部ツールとの連携が容易な点です。
Function Callingでない場合、指定した型に沿ってJSON形式で出力するように、プロンプトを工夫する必要があったり、ユーザーの入力によっては、指定していない型で出力される可能性があります。。
ちなみに、Function Callingの動きや仕組みは、下記の記事が分かりやすかったのでご参考ください。
OpenAIアカウントAPIキーの発行
OpenAIアカウント作成後、APIキーの発行をします。
APIキーの発行は、アカウントの View API keys をクリックします。
Create new secret key をクリックすると、API keyが発行されますので、コピーしておきます。
Lambdaを作成
OpenAIが提供するPython向けのライブラリがありますので、インストールし、Lambda Layerにアップロードします。(直接Lambdaに.zipでのアップロードも可)
MacBook Pro M2の場合、下記のコマンドで、ライブラリをインストールしてZip化します
$ mkdir python $ python3 -m pip install -t ./python openai $ zip -r openai.zip ./python
Lambdaレイヤーでopenai.zip
をアップロードします。
ランタイムPython 3.11
を選択し、Lambda関数を作成します。
作成後、下記の設定を行います
- タイムアウトは、3秒から30秒に変更
- 環境変数では、キーは
API_Key
、値はAPIキーの値を入力 - Lambdaレイヤーを追加します
コードは、下記の通りです。
やっていることとしては、アーティスト名と曲名を含めたユーザー入力内容から、Function Callingで、アーティスト名と曲名を抽出し、JSON形式でレスポンスします。
import json import os import openai openai.api_key = os.environ['API_Key'] def lambda_handler(event, context): messages = [ {"role": "user", "content": "オフィシャルひげだんのプリテンダー"}, ] functions = [ { "name": "get_artist_and_song_title", "description": "アーティスト名と曲名を取得します", "parameters": { "type": "object", "properties": { "artist": { "type": "string", "description": "アーティスト名。アーティスト名が省略されている場合、フルネームにしてください。 例:Ado " }, "song": { "type": "string", "description": "曲名。 曲名が省略されている場合、フルネームにしてください。例:うっせぇわ " }, }, "required": ["artist", "song"] }, }, ] response = openai.ChatCompletion.create( # model="gpt-3.5-turbo-0613", model="gpt-4-0613", messages=messages, functions=functions, temperature=0, max_tokens=512, # function_call="auto" function_call={"name": "get_artist_and_song_title"} ) print(json.dumps(response['choices'][0], ensure_ascii=False)) return json.loads(response["choices"][0]["message"]["function_call"]["arguments"])
コード解説
messages
のcontent
でユーザーの入力内容を記載します- ドキュメントに記載の最新のモデル (gpt-3.5-turbo-0613とgpt-4-0613) のうち、
gpt-4-0613
を使用しました name
は、呼び出したい関数名を記載し、description
には関数の概要を記載します。Chat APIがdescription
の内容を参考にした上で、ユーザーの入力に対して呼び出す関数を決めます。- 今回は、
get_artist_and_song_title
という名前の関数を作成しました。
- 今回は、
- プロパティ(
properties
)内description
でも関数名のdescription
と同様に、的確に説明します- プロパティ名は、単数名もしくは複数名という点も精度に関わってくるので、考慮して決めましょう。
type
も同様です。stringやnumberなどを指定します。
required
は、JSON形式で必ず返したいプロパティ名を指定します。なくても構いません。function_call
では、特定の関数名の呼び出しを必須にしたい場合に限り、関数名を指定します。auto
の場合、ユーザー入力によってChat APIが判断し、関数名や通常の会話を返答します。
temperature
を低くするとユーザーの入力が同じ場合、一貫性のある回答になり、値を高くすると多様な回答が生成されるため、今回は0にしてます。
Lambdaを実行
Lambdaを実行すると、下記の通り、JSON形式でアーティスト名と曲が返りました。正式名称にも変換されてます。
Response { "artist": "Official髭男dism", "song": "Pretender" }
ログ出力内容では、関数名get_artist_and_song_title
が呼ばれていることが確認できますね。
{ "index": 0, "message": { "role": "assistant", "content": null, "function_call": { "name": "get_artist_and_song_title", "arguments": "{\n \"artist\": \"Official髭男dism\",\n \"song\": \"Pretender\"\n}" } }, "finish_reason": "stop" }
ユーザー入力やdescriptionを変更してみた
ユーザーの入力をオフィシャルひげだんのプリテンダー
ではなく、ひげだんのプリテンダー
とした場合は、正式名称に変換されませんでした。AIの性能面が上がれば、クリアはしそうではあります。
実行したコード (クリックすると展開します)
import json import os import openai openai.api_key = os.environ['API_Key'] def lambda_handler(event, context): messages = [ {"role": "user", "content": "ひげだんのプリテンダー"}, ] functions = [ { "name": "get_artist_and_song_title", "description": "アーティスト名と曲名を取得します", "parameters": { "type": "object", "properties": { "artist": { "type": "string", "description": "アーティスト名。アーティスト名が省略されている場合、フルネームにしてください。 例:Ado " }, "song": { "type": "string", "description": "曲名。 曲名が省略されている場合、フルネームにしてください。例:うっせぇわ " }, }, "required": ["artist", "song"] }, }, ] response = openai.ChatCompletion.create( # model="gpt-3.5-turbo-0613", model="gpt-4-0613", messages=messages, functions=functions, temperature=0, max_tokens=512, # function_call="auto" function_call={"name": "get_artist_and_song_title"} ) print(json.dumps(response['choices'][0], ensure_ascii=False)) return json.loads(response["choices"][0]["message"]["function_call"]["arguments"])
Response { "artist": "ひげだん", "song": "プリテンダー" }
また、プロパティ内のdescription
の「フルネーム」という文言を「正式名称」に変えた場合も、正式名称に変換されませんでした。
実行したコード(クリックすると展開します)
import json import os import openai openai.api_key = os.environ['API_Key'] def lambda_handler(event, context): messages = [ {"role": "user", "content": "オフィシャルひげだんのプリテンダー"}, ] functions = [ { "name": "get_artist_and_song_title", "description": "アーティスト名と曲名を取得します", "parameters": { "type": "object", "properties": { "artist": { "type": "string", "description": "アーティスト名。アーティスト名が省略されている場合、正式名称に変換してください。 例:Ado " }, "song": { "type": "string", "description": "曲名。 曲名が省略されている場合、正式名称に変換してください。例:うっせぇわ " }, }, "required": ["artist", "song"] }, }, ] response = openai.ChatCompletion.create( # model="gpt-3.5-turbo-0613", model="gpt-4-0613", messages=messages, functions=functions, temperature=0, max_tokens=512, # function_call="auto" function_call={"name": "get_artist_and_song_title"} ) print(json.dumps(response['choices'][0], ensure_ascii=False)) return json.loads(response["choices"][0]["message"]["function_call"]["arguments"])
Response { "artist": "オフィシャルひげだん", "song": "プリテンダー" }
description
の値によって、結果が変わるので、プロンプトエンジニアリングスキルが必要だと分かります。
複数の関数
呼び出す関数名をもう一つ追加して、ユーザーからの入力に対して、description
を参考に適切な関数が返せるか確認します。
ユーザーからの入力は、明日の10時に東京スカイツに行きたい
で、時間と建物名を抽出するかんせすを追加しました。
import json import os import openai openai.api_key = os.environ['API_Key'] def lambda_handler(event, context): messages = [ {"role": "user", "content": "明日の10時に東京スカイツに行きたい"}, ] functions = [ { "name": "get_artist_and_song_title", "description": "アーティスト名と曲名を取得します", "parameters": { "type": "object", "properties": { "artist": { "type": "string", "description": "アーティスト名。アーティスト名が省略されている場合、フルネームにしてください。 例:Ado " }, "song": { "type": "string", "description": "曲名。 曲名が省略されている場合、フルネームにしてください。例:うっせぇわ " }, }, "required": ["artist", "song"] }, }, { "name": "get_building", "description": "建物名と時間を取得します", "parameters": { "type": "object", "properties": { "building": { "type": "string", "description": "建物名。曲名が省略されている場合、フルネームにしてください 例:東京タワー " }, "time": { "type": "string", "description": "時間。 例:2023年11月1日 10:00:00 " } }, "required": ["building", "time"] } } ] response = openai.ChatCompletion.create( # model="gpt-3.5-turbo-0613", model="gpt-4-0613", messages=messages, functions=functions, temperature=0, max_tokens=512, function_call="auto" # function_call={"name": "get_artist_and_song_title"} ) print(json.dumps(response['choices'][0], ensure_ascii=False)) return json.loads(response["choices"][0]["message"]["function_call"]["arguments"])
建物名が正式名称に変換されつつ、建物名と時間がJSON形式でレスポンスされました。
Response { "building": "東京スカイツリー", "time": "明日の10時" }
ログ出力内容では、関数名get_building
が呼ばれていることが確認できますね。
{ "index": 0, "message": { "role": "assistant", "content": null, "function_call": { "name": "get_building", "arguments": "{\n \"building\": \"東京スカイツリー\",\n \"time\": \"明日の10時\"\n}" } }, "finish_reason": "function_call" }
最後に
今回の検証では、Lambdaを実行しJSON形式で返るとき、description
の内容によっては、プロパティ名が正式名称に変換されることが確認できました。
この調整というのは、いわゆるプロンプトエンジニアリングの分野なので、意図した通りの回答になるよう色々試したいと思います。